﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MLSharp.Classification;
using MLSharp.Utilities;

namespace MLSharp.ConsoleRunner
{
	/// <summary>
	/// A utility class that can be used to load a classifier
	/// factory based on a string containing settings.
	/// </summary>
	public class ClassifierFactoryLoader
	{
		#region Private Const Fields

		/// <summary>
		/// These are the names of assemblies that should not be scanned
		/// for classifier factories.
		/// </summary>
		private static readonly string[] mAssembliesToSkip = new[] { "mscorlib", "log4net", "nunit.framework", "Plossum", "System.Core" };

		#endregion

		#region Private Readonly Fields

		/// <summary>
		/// The logger for the class.
		/// </summary>
		private readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// Maps a factory type name to the actual type reference.
		/// </summary>
		private readonly Dictionary<string, Type> mFactoryMap = new Dictionary<string, Type>();

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates and initailizes a new loader.  All
		/// types in any referenced assembly that implements
		/// <see cref="IClassifierFactory"/> will be indexed
		/// and become accessible via <see cref="Load"/>.
		/// </summary>
		public ClassifierFactoryLoader()
		{
			Initialize();
		}

		#endregion

		#region Private Methods

		/// <summary>
		/// Initializes all indexes.
		/// </summary>
		private void Initialize()
		{
			List<Assembly> assemblies = new List<Assembly> {Assembly.GetExecutingAssembly()};

			assemblies.AddRange(assemblies[0].GetReferencedAssemblies().Select(n => Assembly.Load(n)));

			foreach (Assembly assembly in assemblies)
			{
				if (assembly.FullName == null)
					continue;

				//Skip referenced assemblies that we know won't have any classifier factories.
				string partialName = assembly.FullName.Substring(0, assembly.FullName.IndexOf(","));
				if (mAssembliesToSkip.Contains(partialName))
					continue;

				foreach (Type t in assembly.GetExportedTypes())
				{
					if (t.GetInterface(typeof (IClassifierFactory).Name) != null &&
					    !t.IsAbstract &&
					    !t.IsInterface)
					{
						mFactoryMap.Add(t.Name, t);
					}
				}
			}
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Instantiates a new <see cref="IClassifierFactory"/>
		/// based on the info specified.
		/// </summary>
		/// <param name="info">Arguments</param>
		/// <returns>The classifier factory if it can be loaded, null otherwise.</returns>
		/// <remarks>
		/// This method must be manually extended to handle new classifier factories.
		/// </remarks>
		public IClassifierFactory Load(string info)
		{
			IClassifierFactory factory = null;

			info = info.Trim();

			string[] parts = info.Split(new [] {' '}, StringSplitOptions.RemoveEmptyEntries);

			string classifierName = parts[0];

			if (mFactoryMap.ContainsKey(classifierName))
			{
				factory = Reflector.CreateInstance<IClassifierFactory>(mFactoryMap[classifierName]);
			}

			if (factory == null)
			{
				return null;
			}
			else if (mLogger.IsInfoEnabled)
			{
				mLogger.InfoFormat("Loaded classifier factory: {0}.", info);
			}

			if (parts.Length > 1)
			{
				//Parse out the options.
				for (int i=1; i < parts.Length; i++)
				{
					string[] optionParts = parts[i].Split(':');

					//Options must be of the form "key:value[,value[...]]"
					if (optionParts.Length != 2)
					{
						throw new InvalidOperationException("Option format is not valid.");
					}

					Reflector.SetPropertyValue(factory.Options, optionParts[0], optionParts[1]);
				}
			}

			return factory;
		}

		#endregion
	}
}
